跳到主要内容

Go 的 defer 坑

defer 函数压栈

这个关键字类似 Java 中的 finally 关键字,会在 try 代码块的最后执行,这个 deferreturn 之后执行

import "fmt"

func main() {
defer fmt.Println("main end~")

fmt.Println("main 1")
fmt.Println("main 2")
}

打印结果

main 1
main 2
main end~

它的执行其实是压栈的过程,如下代码,逐步压栈

import "fmt"

func fun01() {
fmt.Println("A")
}

func fun02() {
fmt.Println("B")
}

func fun03() {
fmt.Println("C")
}

func main() {
defer fun01()
defer fun02()
defer fun03()
}

打印结果

C
B
A

defer 的执行坑

题目一

题目说明:函数 deferFuncParameter() 定义一个整型变量并初始化为 1,然后使用 defer 语句打印出变量值,最后修改变量值为 2

func deferFuncParameter() {
var aInt = 1

defer fmt.Println(aInt)

aInt = 2
return
}

参考答案: 输出 1。延迟函数 fmt.Println(aInt) 的参数在 defer 语句出现时就已经确定了,所以无论后面如何修改 aInt 变量都不会影响延迟函数。

题目二

函数 deferFuncParameter() 定义一个数组,通过 defer 延迟函数 printArray() 的调用,最后修改数组第一个元素。printArray() 函数接受数组的指针并把数组全部打印出来。

package main

import "fmt"

func printArray(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}

func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}

defer printArray(&aArray)

aArray[0] = 10
return
}

func main() {
deferFuncParameter()
}

输出 10、2、3 三个值。延迟函数 printArray() 的参数在defer语句出现时就已经确定了,即数组的地址,由于延迟函数执行时机是在 return 语句之前,所以对数组的最终修改值会被打印出来。

提示

如果改成下面这样则是 1、2、3 三个值,因为此时延迟函数的参数是数组的值,而不是地址。

func printArray(array [3]int) {
for i := range array {
fmt.Println(array[i])
}
}

func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}

defer printArray(aArray)

aArray[0] = 10
return
}

func main() {
deferFuncParameter()
}

题目三

下面函数输出什么?

func deferFuncReturn() (result int) {
i := 1

defer func() {
result++
}()

return i
}

参考答案:函数输出 2。函数的 return 语句并不是原子的,实际执行分为 设置返回值-->ret,defer语句实际执行在返回前,即拥有 defer 的函数返回过程是:设置返回值-->执行defer-->ret

所以 return 语句先把 result 设置为 i 的值,即 1,defer 语句中又把 result 递增1,所以最终返回 2。

defer 规则

1、延迟函数的参数在defer语句出现时就已经确定下来了 2、延迟函数执行按后进先出顺序执行,即先出现的 defer 最后执行 3、延迟函数可能操作主函数的具名返回值

主要通过第三个规则来理解一下函数返回过程

有一个事实必须要了解,关键字 return 不是一个原子操作,实际上 return 只代理汇编指令 ret,即将跳转程序执行。比如语句 return i,实际上分两步进行,即将 i 值存入栈中作为返回值,然后执行跳转,而 defer 的执行时机正是跳转前,所以说 defer 执行时还是有机会操作返回值的。

举个实际的例子进行说明这个过程:

func deferFuncReturn() (result int) {
i := 1

defer func() {
result++
}()

return i
}

该函数的 return 语句可以拆分成下面两行:

result = i
return

而延迟函数的执行正是在 return 之前,即加入 defer 后的执行过程如下:

result = i
result++
return

所以上面函数实际返回 i++ 值。

关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把 return 语句拆开都可以很好的理解,下面分别举例说明

主函数拥有匿名返回值

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回 "1"、"2"、"Hello" 这样的值,这种情况下 defer 语句是无法操作返回值的。

一个返回字面值的函数,如下所示:

func foo() int {
var i int

defer func() {
i++
}()

return 1
}

上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。

同样的,一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下 defer 语句可以引用到返回值,但不会改变返回值。

一个返回本地变量的函数,如下所示:

func foo() int {
var i int

defer func() {
i++
}()

return i
}

上面的函数,返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为"anony",上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于 i 是整型,会将值拷贝给 anony,所以 defer 语句中修改 i 值,对函数返回值不造成影响。

主函数拥有具名返回值

主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。

一个影响函返回值的例子:

func foo() (ret int) {
defer func() {
ret++
}()

return 0
}

上面的函数拆解出来,如下所示:

ret = 0
ret++
return

函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1。

References